﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SharedGenomics.TextMining
{
    /// <summary>
    /// This class is used to 'tile' a string into a sequence of terms and non-terms - given a collection of terms and a string it will break the input string 
    /// up and find the best way to extract the terms. For example if 'dog' and 'fox' are considered terms, then the string 'the quick brown fox jumped 
    /// over the lazy dog' should be broken up into {the quick brown}{fox}{jumped over the lazy}{dog}. Note that it is possible that some terms in the collection 
    /// are sub-terms of larger terms - for example if 'lazy dog' was also a term then 'dog' would be a sub-term. In this case the tiler should try to return the 
    /// optimal tiling (i.e. the tiling with the highest rank for some ranking function) - at present however the tiling is greedy and simply uses the longest possible 
    /// term that can be used for each section in the input string. It may be desirable to modify this class to use a backtracking search or dynamic programming to 
    /// find an optimal tiling given some evaluation function...
    /// </summary>
    public class StringTiler
    {
        #region Private Members

        private ScoredTermCollection _terms;

        #endregion

        #region Constructors

        public StringTiler(ScoredTermCollection terms)
        {
            this.Terms = terms;
        }

        #endregion

        #region Public Methods

        public StringTiling Tile(string str)
        {
            StringTiling tiling = new StringTiling(new ScoreTiling(this.CalculateTilingScore));
            TileSection currentSection;

            if (!String.IsNullOrEmpty(str))
            {
                int charIndex = 0;
                int currentTileStartIndex = 0;

                while (charIndex < str.Length)
                {
                    //see if any term in the collection begins with the current letter in the input
                    ICollection<ScoredTerm> terms = this.Terms.GetTermsStartingWith(str[charIndex]);

                    if (this.TryMatchTerm(str, charIndex, terms, out currentSection))
                    {
                        //check to see if a non-term exists between this term and the last one
                        if (charIndex != currentTileStartIndex)
                        {
                            tiling.Add(this.CreateNonTermTile(str, currentTileStartIndex, charIndex));
                        }

                        //add term section to tiling
                        tiling.Add(currentSection);

                        //move the wordIndex and currentTileStartIndex to the position after the current term
                        charIndex += currentSection.Text.Length;
                        currentTileStartIndex = charIndex;
                    }
                    else
                    {
                        //move to the next word
                        charIndex++;
                    }
                }

                //check to see if the input ends in a non-term tile
                if (currentTileStartIndex < str.Length)
                {
                    currentSection = this.CreateNonTermTile(str, currentTileStartIndex, str.Length);
                    tiling.Add(currentSection);
                }
            }

            return tiling;
        }

        #endregion

        #region Private Methods

        private TileSection CreateNonTermTile(string str, int startIndex, int endIndex)
        {
            Debug.Assert(str != null);
            Debug.Assert(startIndex >= 0);
            Debug.Assert(endIndex <= str.Length);
            Debug.Assert(endIndex >= startIndex);

            string tileText = str.Substring(startIndex, endIndex - startIndex);
            return new TileSection(tileText);
        }

        private bool TryMatchTerm(string str, int startIndex, ICollection<ScoredTerm> terms, out TileSection section)
        {
            //initialse out parameter
            section = null;

            //copy matching terms to list and sort on length (descending - the longest term should be at the front)
            List<ScoredTerm> matchingTermList = new List<ScoredTerm>(terms);
            matchingTermList.Sort(new Comparison<ScoredTerm>(this.ScoredTermComparison));

            ScoredTerm currentMatchedTerm;

            //try each term in descending length order and see if it is contained in the input word array from the current index
            foreach (ScoredTerm t in matchingTermList)
            {
                //don't consider any terms with negative c-values
                if (t.CValue <= 0.0)
                    continue;

                if(this.IsTermFound(str, startIndex, t, out currentMatchedTerm))
                {
                    //get the quartile for the term
                    int quartile = this.Terms.GetTermQuartile(t);
                    section = new TileSection(currentMatchedTerm, quartile);

                    return true;
                }
            }

            //none of the terms matched
            return false;
        }

        private bool IsTermFound(string str, int startIndex, ScoredTerm inputTerm, out ScoredTerm matchedTerm)
        {
            Debug.Assert(str != null);
            Debug.Assert(startIndex >= 0);
            Debug.Assert(startIndex < str.Length);
            Debug.Assert(inputTerm != null);

            //initialise out variable
            matchedTerm = null;

            if (inputTerm.Term.Length + startIndex <= str.Length)
            {
                int charIndex = startIndex;

                //check each character in the term match successive characters in the input string from the starting position
                foreach (char termChar in inputTerm.Term)
                {
                    if (Char.ToLower(termChar) != Char.ToLower(str[charIndex]))
                        return false;

                    //move to next character
                    charIndex++;
                }

                //extract the text in str that matches the input term
                string matchingText = str.Substring(startIndex, charIndex - startIndex);
                matchedTerm = new ScoredTerm(matchingText, inputTerm.CValue);

                //each word matched
                return true;
            }
            else
            {
                //term is too long to be contained within the words array
                return false;
            }
        }

        private double CalculateTilingScore(IEnumerable<TileSection> sections)
        {
            double total = 0.0;

            if (sections != null)
            {
                foreach (TileSection section in sections)
                {
                    //only terms contribute to the score
                    if (section != null && section.IsTerm)
                        total += section.CValue;
                }
            }

            int termCount = (sections.Where(t => t.IsTerm).Count());
            return termCount == 0 ? 0 : total / termCount;
        }

        private int ScoredTermComparison(ScoredTerm x, ScoredTerm y)
        {
            if (x == null)
            {
                return (y == null) ? 0 : 1;
            }

            //x is not null
            if (y == null)
                return 1;
            else
                return (-1 * x.Words.Count.CompareTo(y.Words.Count));
        }

        #endregion

        #region Properties

        private ScoredTermCollection Terms
        {
            get { return this._terms; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("Terms");

                this._terms = value;
            }
        }

        #endregion
    }
}
